// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/audio/win/audio_device_listener_win.h"

#include <Audioclient.h>

#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "base/system_monitor/system_monitor.h"
#include "base/time/default_tick_clock.h"
#include "base/win/scoped_co_mem.h"
#include "base/win/windows_version.h"
#include "media/audio/win/core_audio_util_win.h"

using base::win::ScopedCoMem;

namespace media {

static std::string FlowToString(EDataFlow flow)
{
    return (flow == eRender) ? "eRender" : "eConsole";
}

static std::string RoleToString(ERole role)
{
    switch (role) {
    case eConsole:
        return "eConsole";
    case eMultimedia:
        return "eMultimedia";
    case eCommunications:
        return "eCommunications";
    default:
        return "undefined";
    }
}

AudioDeviceListenerWin::AudioDeviceListenerWin(const base::Closure& listener_cb)
    : listener_cb_(listener_cb)
    , tick_clock_(new base::DefaultTickClock())
{
    CHECK(CoreAudioUtil::IsSupported());

    ScopedComPtr<IMMDeviceEnumerator> device_enumerator(
        CoreAudioUtil::CreateDeviceEnumerator());
    if (!device_enumerator.get())
        return;

    HRESULT hr = device_enumerator->RegisterEndpointNotificationCallback(this);
    if (FAILED(hr)) {
        LOG(ERROR) << "RegisterEndpointNotificationCallback failed: "
                   << std::hex << hr;
        return;
    }

    device_enumerator_ = device_enumerator;
}

AudioDeviceListenerWin::~AudioDeviceListenerWin()
{
    DCHECK(thread_checker_.CalledOnValidThread());
    if (device_enumerator_.get()) {
        HRESULT hr = device_enumerator_->UnregisterEndpointNotificationCallback(this);
        LOG_IF(ERROR, FAILED(hr)) << "UnregisterEndpointNotificationCallback() "
                                  << "failed: " << std::hex << hr;
    }
}

STDMETHODIMP_(ULONG)
AudioDeviceListenerWin::AddRef()
{
    return 1;
}

STDMETHODIMP_(ULONG)
AudioDeviceListenerWin::Release()
{
    return 1;
}

STDMETHODIMP AudioDeviceListenerWin::QueryInterface(REFIID iid, void** object)
{
    if (iid == IID_IUnknown || iid == __uuidof(IMMNotificationClient)) {
        *object = static_cast<IMMNotificationClient*>(this);
        return S_OK;
    }

    *object = NULL;
    return E_NOINTERFACE;
}

STDMETHODIMP AudioDeviceListenerWin::OnPropertyValueChanged(
    LPCWSTR device_id, const PROPERTYKEY key)
{
    // TODO(dalecurtis): We need to handle changes for the current default device
    // here.  It's tricky because this method may be called many (20+) times for
    // a single change like sample rate.  http://crbug.com/153056
    return S_OK;
}

STDMETHODIMP AudioDeviceListenerWin::OnDeviceAdded(LPCWSTR device_id)
{
    // We don't care when devices are added.
    return S_OK;
}

STDMETHODIMP AudioDeviceListenerWin::OnDeviceRemoved(LPCWSTR device_id)
{
    // We don't care when devices are removed.
    return S_OK;
}

STDMETHODIMP AudioDeviceListenerWin::OnDeviceStateChanged(LPCWSTR device_id, DWORD new_state)
{
    DebugBreak();
//     base::SystemMonitor* monitor = base::SystemMonitor::Get();
//     if (monitor)
//         monitor->ProcessDevicesChanged(base::SystemMonitor::DEVTYPE_AUDIO_CAPTURE);

    return S_OK;
}

STDMETHODIMP AudioDeviceListenerWin::OnDefaultDeviceChanged(
    EDataFlow flow, ERole role, LPCWSTR new_default_device_id)
{
    // Only listen for console and communication device changes.
    if ((role != eConsole && role != eCommunications) || (flow != eRender && flow != eCapture)) {
        return S_OK;
    }

    // If no device is now available, |new_default_device_id| will be NULL.
    std::string new_device_id;
    if (new_default_device_id)
        new_device_id = base::WideToUTF8(new_default_device_id);

    // Only output device changes should be forwarded.  Do not attempt to filter
    // changes based on device id since some devices may not change their device
    // id and instead trigger some internal flow change: http://crbug.com/506712
    //
    // We rate limit device changes to avoid a single device change causing back
    // to back changes for eCommunications and eConsole; this is worth doing as
    // it provides a substantially faster resumption of playback.
    bool did_run_listener_cb = false;
    const base::TimeTicks now = tick_clock_->NowTicks();
    if (flow == eRender && now - last_device_change_time_ > base::TimeDelta::FromMilliseconds(kDeviceChangeLimitMs)) {
        last_device_change_time_ = now;
        listener_cb_.Run();
        did_run_listener_cb = true;
    }

    DVLOG(1) << "OnDefaultDeviceChanged() "
             << "new_default_device: "
             << (new_default_device_id
                        ? CoreAudioUtil::GetFriendlyName(new_device_id)
                        : "no device")
             << ", flow: " << FlowToString(flow)
             << ", role: " << RoleToString(role)
             << ", notified manager: " << (did_run_listener_cb ? "Yes" : "No");

    return S_OK;
}

} // namespace media
